﻿using System;
using System.IO;
using dIRca.Data;
using NLog;
using System.Text.RegularExpressions;
using System.Reflection;
#if (WINDOWS_PHONE)
using Homebrew.Net.Sockets;
#else
using System.Net.Sockets;
#endif

namespace dIRca.Network
{
    class MessageService : IDisposable
    {
        private static Logger logger = LogManager.GetCurrentClassLogger();

        private StreamReader _streamReader = null;
        private StreamWriter _streamWriter = null;

        private string _serverName;
        private string _email;

        private ReplyCode[] _replyCodes;

        private static Regex _replyCodeRegex = new Regex("^:[^ ]+? ([0-9]{3}) .+$", RegexOptions.Compiled);
        private static Regex _pingRegex = new Regex("^PING :.*", RegexOptions.Compiled);
        private static Regex _errorRegex = new Regex("^ERROR :.*", RegexOptions.Compiled);
        private static Regex _actionRegex = new Regex("^:.*? PRIVMSG (.).* :" + "\x1" + "ACTION .*" + "\x1" + "$", RegexOptions.Compiled);
        private static Regex _ctcpRequestRegex = new Regex("^:.*? PRIVMSG .* :" + "\x1" + ".*" + "\x1" + "$", RegexOptions.Compiled);
        private static Regex _messageRegex = new Regex("^:.*? PRIVMSG (.).* :.*$", RegexOptions.Compiled);
        private static Regex _ctcpReplyRegex = new Regex("^:.*? NOTICE .* :" + "\x1" + ".*" + "\x1" + "$", RegexOptions.Compiled);
        private static Regex _noticeRegex = new Regex("^:.*? NOTICE (.).* :.*$", RegexOptions.Compiled);
        private static Regex _inviteRegex = new Regex("^:.*? INVITE .* .*$", RegexOptions.Compiled);
        private static Regex _joinRegex = new Regex("^:.*? JOIN .*$", RegexOptions.Compiled);
        private static Regex _topicRegex = new Regex("^:.*? TOPIC .* :.*$", RegexOptions.Compiled);
        private static Regex _nickNameRegex = new Regex("^:.*? NICK .*$", RegexOptions.Compiled);
        private static Regex _kickRegex = new Regex("^:.*? KICK .* .*$", RegexOptions.Compiled);
        private static Regex _partRegex = new Regex("^:.*? PART .*$", RegexOptions.Compiled);
        private static Regex _modeRegex = new Regex("^:.*? MODE (.*) .*$", RegexOptions.Compiled);
        private static Regex _quitRegex = new Regex("^:.*? QUIT :.*$", RegexOptions.Compiled);

        private NetworkStream _stream = null;
        public NetworkStream Stream
        {
            get { return _stream; }
            set
            {
                if (_stream == value)
                    return;

                _stream = value;
                if (_stream != null)
                {
                    _count = 0;
                    _streamReader = new StreamReader(_stream);
                    _streamWriter = new StreamWriter(_stream);
                }
            }
        }

        public string NickName { get; set; }

        private int _count;
        public int Count
        {
            get { return _count; }
        }

        public event EventHandler<MessageEventArgs> MessageReceived;
        public event EventHandler<MessageEventArgs> MessageSent;

        public MessageService(NetworkStream networkStream, string serverName, string nickName, string email)
        {
            _stream = networkStream;
            _serverName = serverName;
            NickName = nickName;
            _email = email;
            _replyCodes = GetValues(typeof(ReplyCode));
        }

        public ReplyCode[] GetValues(Type typeOfEnum)
        {
            ReplyCode[] values = new ReplyCode[0];
            if (typeOfEnum.BaseType == typeof(Enum))
            {
                // public static members of enumeration
                var fields = typeOfEnum.GetFields(BindingFlags.Public | BindingFlags.Static);

                // set a size of a result array to number of members
                values = new ReplyCode[fields.Length];

                for (int index = 0; index < fields.Length; index++)
                {
                    values[index] = (ReplyCode)fields[index].GetValue(null);
                }
            }
            return values;
        }

        public void SendMessage(string message)
        {
            WriteLine(message);
        }

        public void WriteLine(string message)
        {
            WriteBase(message + "\r\n");
        }

        public void Write(string message)
        {
            WriteBase(message);
        }

        private void WriteBase(string message)
        {
            var isSent = false;
            try
            {
                _streamWriter.Write(message);
                _streamWriter.Flush();
                isSent = true;
            }
            catch (Exception)
            {
                // HACK: Breakin' the law
                //throw;
            }
            if (isSent)
            {
                var logMessage = "RAW: SENT " + message;
                logger.Info(logMessage);
                var parsedSentMessage = ParseSentMessage(message.TrimEnd(Environment.NewLine.ToCharArray()));
                var parsedMessage = ParseMessage(parsedSentMessage);
                if (parsedMessage != null)
                    OnMessageSent(parsedMessage);
            }
        }

        public void SendMessage(SendType type, string destination, string message)
        {
            switch (type)
            {
                case SendType.Message:
                    Privmsg(destination, message);
                    break;
                case SendType.Action:
                    Privmsg(destination, "\x1" + "ACTION " + message + "\x1");
                    break;
                case SendType.Notice:
                    Notice(destination, message);
                    break;
                case SendType.CtcpRequest:
                    Privmsg(destination, "\x1" + message + "\x1");
                    break;
                case SendType.CtcpReply:
                    Notice(destination, "\x1" + message + "\x1");
                    break;
            }
        }

        void Privmsg(string destination, string message)
        {
            var result = "PRIVMSG " + destination + " :" + message;
            WriteLine(result);
        }

        void Notice(string destination, string message)
        {
            var result = "NOTICE " + destination + " :" + message;
            WriteLine(result);
        }

        public void SendReply(Message data, string message)
        {
            switch (data.ReceiveType)
            {
                case ReceiveType.ChannelMessage:
                    SendMessage(SendType.Message, data.ChannelName, message);
                    break;
                case ReceiveType.QueryMessage:
                    SendMessage(SendType.Message, data.NickName, message);
                    break;
                case ReceiveType.QueryNotice:
                    SendMessage(SendType.Notice, data.NickName, message);
                    break;
            }
        }

        public string ReceiveMessage()
        {
            // This method has to be called in a loop and exceptions swallowed due to the Native Sockets API
            // Processing is mostly done via the events. This could be thought of as an async call without the Begin/End. The On<>ed events 
            var result = string.Empty;
            try
            {
                result = _streamReader.ReadLine();
            }
            catch (Exception)
            {
                //throw;
            }
            if (result == "\r\n")
                result = string.Empty;
            if (!string.IsNullOrEmpty(result))
            {
                var logMessage = "RAW: READ " + result;
                logger.Info(logMessage);
                var parsedMessage = ParseMessage(result);
                if (parsedMessage != null)
                    OnMessageReceived(parsedMessage);
            }

            return result;
        }

        Message ParseMessage(string rawline)
        {
            Message result = null;
            if (rawline == null)
            {
                throw new ArgumentNullException("rawline");
            }

            string line;
            string[] linear;
            string messagecode;
            string from;
            string nick = null;
            string ident = null;
            string host = null;
            string channel = null;
            string message = null;
            ReceiveType type;
            ReplyCode replycode;
            int exclamationpos;
            int atpos;
            int colonpos;

            if (rawline.Length == 0)
            {
                throw new ArgumentException("Value must not be empty.", "rawline");
            }

            if (rawline[0] == ':')
            {
                line = rawline.Substring(1);
            }
            else
            {
                line = rawline;
            }

            linear = line.Split(new char[] { ' ' });

            // conform to RFC 2812
            from = linear[0];
            messagecode = linear[1];
            exclamationpos = from.IndexOf("!");
            atpos = from.IndexOf("@");
            colonpos = line.IndexOf(" :");
            if (colonpos != -1)
            {
                // we want the exact position of ":" not beginning from the space
                colonpos += 1;
            }
            if (exclamationpos != -1)
            {
                nick = from.Substring(0, exclamationpos);
            }
            else
            {
                if (atpos == -1)
                {
                    // no ident and no host, should be nick then
                    if (!from.Contains("."))
                    {
                        // HACK: from seems to be a nick instead of servername
                        nick = from;
                    }
                }
                else
                {
                    nick = from.Substring(0, atpos);
                }
            }
            if ((atpos != -1) &&
                (exclamationpos != -1))
            {
                ident = from.Substring(exclamationpos + 1, (atpos - exclamationpos) - 1);
            }
            if (atpos != -1)
            {
                host = from.Substring(atpos + 1);
            }

            try
            {
                replycode = (ReplyCode)int.Parse(messagecode);
            }
            catch (FormatException)
            {
                replycode = ReplyCode.Null;
            }
            type = GetMessageType(rawline);
            if (colonpos != -1)
            {
                message = line.Substring(colonpos + 1);
            }

            switch (type)
            {
                case ReceiveType.Join:
                case ReceiveType.Kick:
                case ReceiveType.Part:
                case ReceiveType.TopicChange:
                case ReceiveType.ChannelModeChange:
                case ReceiveType.ChannelMessage:
                case ReceiveType.ChannelAction:
                case ReceiveType.ChannelNotice:
                    channel = linear[2];
                    break;
                case ReceiveType.Who:
                case ReceiveType.Topic:
                case ReceiveType.Invite:
                case ReceiveType.BanList:
                case ReceiveType.ChannelMode:
                    channel = linear[3];
                    break;
                case ReceiveType.Name:
                    channel = linear[4];
                    break;
            }

            switch (replycode)
            {
                case ReplyCode.List:
                case ReplyCode.ListEnd:
                case ReplyCode.ErrorNoChannelModes:
                    channel = linear[3];
                    break;
            }

            if ((!string.IsNullOrEmpty(channel)) &&
                (channel[0] == ':'))
            {
                channel = channel.Substring(1);
            }

            ++_count;
            result = new Message()
            {
                Count = _count,
                Sender = from,
                NickName = nick,
                Ident = ident,
                HostName = host,
                ChannelName = channel,
                MessageData = message,
                RawMessageData = rawline,
                ReceiveType = type,
                ReplyCode = replycode,
            };

            //var logMessage = "Parsed: \r\nSender: " + result.Sender + "\r\nData: " + result.Data;
            //logger.Info(logMessage);

            return result;
        }

        private ReceiveType GetMessageType(string rawline)
        {
            Match found = _replyCodeRegex.Match(rawline);
            var logMessage = string.Empty;
            if (found.Success)
            {
                string code = found.Groups[1].Value;
                ReplyCode replycode = (ReplyCode)int.Parse(code);

                // check if this replycode is known in the RFC
                if (Array.IndexOf<ReplyCode>(_replyCodes, replycode) == -1)
                {
                    logMessage = "This IRC server (" + _serverName + ") doesn't conform to the RFC 2812! ignoring unrecognized replycode '" + replycode + "'";
                    logger.Warn(logMessage);
                    return ReceiveType.Unknown;
                }

                switch (replycode)
                {
                    case ReplyCode.Welcome:
                    case ReplyCode.YourHost:
                    case ReplyCode.Created:
                    case ReplyCode.MyInfo:
                    case ReplyCode.Bounce:
                        return ReceiveType.Login;
                    case ReplyCode.LuserClient:
                    case ReplyCode.LuserOp:
                    case ReplyCode.LuserUnknown:
                    case ReplyCode.LuserMe:
                    case ReplyCode.LuserChannels:
                        return ReceiveType.Info;
                    case ReplyCode.MotdStart:
                    case ReplyCode.Motd:
                    case ReplyCode.EndOfMotd:
                        return ReceiveType.Motd;
                    case ReplyCode.NamesReply:
                    case ReplyCode.EndOfNames:
                        return ReceiveType.Name;
                    case ReplyCode.WhoReply:
                    case ReplyCode.EndOfWho:
                        return ReceiveType.Who;
                    case ReplyCode.ListStart:
                    case ReplyCode.List:
                    case ReplyCode.ListEnd:
                        return ReceiveType.List;
                    case ReplyCode.BanList:
                    case ReplyCode.EndOfBanList:
                        return ReceiveType.BanList;
                    case ReplyCode.Topic:
                    case ReplyCode.NoTopic:
                        return ReceiveType.Topic;
                    case ReplyCode.WhoIsUser:
                    case ReplyCode.WhoIsServer:
                    case ReplyCode.WhoIsOperator:
                    case ReplyCode.WhoIsIdle:
                    case ReplyCode.WhoIsChannels:
                    case ReplyCode.EndOfWhoIs:
                        return ReceiveType.WhoIs;
                    case ReplyCode.WhoWasUser:
                    case ReplyCode.EndOfWhoWas:
                        return ReceiveType.WhoWas;
                    case ReplyCode.UserModeIs:
                        return ReceiveType.UserMode;
                    case ReplyCode.ChannelModeIs:
                        return ReceiveType.ChannelMode;
                    default:
                        if (((int)replycode >= 400) &&
                            ((int)replycode <= 599))
                        {
                            return ReceiveType.ErrorMessage;
                        }
                        else
                        {
                            logMessage = "replycode unknown ("+code+"): \""+rawline+"\"";
                            logger.Warn(logMessage);
                            return ReceiveType.Unknown;
                        }
                }
            }

            found = _pingRegex.Match(rawline);
            if (found.Success)
            {
                return ReceiveType.Unknown;
            }

            found = _errorRegex.Match(rawline);
            if (found.Success)
            {
                return ReceiveType.Error;
            }

            found = _actionRegex.Match(rawline);
            if (found.Success)
            {
                switch (found.Groups[1].Value)
                {
                    case "#":
                    case "!":
                    case "&":
                    case "+":
                        return ReceiveType.ChannelAction;
                    default:
                        return ReceiveType.QueryAction;
                }
            }

            found = _ctcpRequestRegex.Match(rawline);
            if (found.Success)
            {
                return ReceiveType.CtcpRequest;
            }

            found = _messageRegex.Match(rawline);
            if (found.Success)
            {
                switch (found.Groups[1].Value)
                {
                    case "#":
                    case "!":
                    case "&":
                    case "+":
                        return ReceiveType.ChannelMessage;
                    default:
                        return ReceiveType.QueryMessage;
                }
            }

            found = _ctcpReplyRegex.Match(rawline);
            if (found.Success)
            {
                return ReceiveType.CtcpReply;
            }

            found = _noticeRegex.Match(rawline);
            if (found.Success)
            {
                switch (found.Groups[1].Value)
                {
                    case "#":
                    case "!":
                    case "&":
                    case "+":
                        return ReceiveType.ChannelNotice;
                    default:
                        return ReceiveType.QueryNotice;
                }
            }

            found = _inviteRegex.Match(rawline);
            if (found.Success)
            {
                return ReceiveType.Invite;
            }

            found = _joinRegex.Match(rawline);
            if (found.Success)
            {
                return ReceiveType.Join;
            }

            found = _topicRegex.Match(rawline);
            if (found.Success)
            {
                return ReceiveType.TopicChange;
            }

            found = _nickNameRegex.Match(rawline);
            if (found.Success)
            {
                return ReceiveType.NickChange;
            }

            found = _kickRegex.Match(rawline);
            if (found.Success)
            {
                return ReceiveType.Kick;
            }

            found = _partRegex.Match(rawline);
            if (found.Success)
            {
                return ReceiveType.Part;
            }

            found = _modeRegex.Match(rawline);
            if (found.Success)
            {
                if (found.Groups[1].Value == NickName)
                {
                    return ReceiveType.UserModeChange;
                }
                else
                {
                    return ReceiveType.ChannelModeChange;
                }
            }

            found = _quitRegex.Match(rawline);
            if (found.Success)
            {
                return ReceiveType.Quit;
            }
            logMessage = "messagetype unknown: \"" + rawline + "\"";
            logger.Warn(logMessage);
            return ReceiveType.Unknown;
        }

        string ParseSentMessage(string rawline)
        {
            var result = string.Empty;
            if (rawline == null)
            {
                throw new ArgumentNullException("rawline");
            }
            var sender = ":" + NickName + "!" + _email;
            result = sender + " " + rawline;

            return result;
        }

        void OnMessageReceived(Message message)
        {
            var handler = MessageReceived;
            if (handler != null)
            {
                var eventArgs = new MessageEventArgs()
                {
                    Message = message,
                };
                handler(this, eventArgs);
            }
        }

        void OnMessageSent(Message message)
        {
            var handler = MessageSent;
            if (handler != null)
            {
                var eventArgs = new MessageEventArgs()
                {
                    Message = message,
                };
                handler(this, eventArgs);
            }
        }

        public void Dispose()
        {

        }
    }
}
